﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace OvercomingShyness
{
    /// <summary>
    /// Single pseudo-thread of an interpreter
    /// </summary>
    public class BadKeyThread
    {
        private BadKeyMachine owner;
        private Stack<Dictionary<string, string>> stack;
        private int ip;

        /// <summary>
        /// Spawn a new thread
        /// </summary>
        /// <param name="owner">
        /// In case of multiple virtual machines running simultaneously,
        /// this tracks which is the thread's parent
        /// </param>
        public BadKeyThread(BadKeyMachine owner)
        {
            this.owner = owner;
            stack = new Stack<Dictionary<string, string>>();
            ip = -1;
        }

        /// <summary>
        /// Calls either a script function or external delegated function by it's in script name
        /// </summary>
        /// <param name="label">function to call</param>
        /// <param name="args">function arguments</param>
        public void Call(string label, string args)
        {
            // regardless of whether the call is internal or external
            // it is important that it handles identically
            if (owner.Labels.ContainsKey(label))
            {
                // make a new stack frame
                Dictionary<string, string> frame = new Dictionary<string, string>();

                string[] strings;
                args = BadKeyCompiler.RemoveStrings(args, out strings);

                // split the args up
                string[] argv = Regex.Split(args, @",");

                // grab the function header out of the script
                string header = owner.Script[owner.Labels[label]];
                header = header.Substring(header.IndexOf('(') + 1);
                header = header.Remove(header.IndexOf(')'), 1);

                string[] argnames;

                if (header.Trim().Equals("..."))
                {
                    argnames = new string[argv.Count()];
                    for (int i = 0; i < argv.Count(); i++)
                    {
                        argnames[i] = "arg" + i;
                    }
                }
                else
                {
                    argnames = header.Split(',');
                }

                // don't populate the arguments if there's no arguments.
                if (args.Length != 0)
                {
                    // populate the stack frame by merging the argument positions
                    // in the call with their names in the header
                    for (int i = 0; i < argv.Length; i++)
                    {
                        frame.Add(argnames[i].Trim(),
                            FetchValue(BadKeyCompiler.ReplaceStrings(argv[i], strings).Trim()));
                    }
                }

                // add a return pointer to the frame
                frame.Add("!return", "" + ip);

                // and push it onto the stack
                stack.Push(frame);
                
                // trace the stack frame
                string acc = "";
                acc += (Tabbage() + label + " called from line " + ip + " with arguments ");
                foreach (KeyValuePair<string, string> pair in frame)
                    acc += (pair.Key + ":\"" + pair.Value + "\" ");
                owner.Debug(acc);

                ip = owner.Labels[label];
                EvalNext();
            }
            else if (owner.Delegates.ContainsKey(label))
            {
                // make a new stack frame
                Dictionary<string, string> frame = new Dictionary<string, string>();

                string[] strings;
                args = BadKeyCompiler.RemoveStrings(args, out strings);

                string[] argv = args.Split(',');

                // don't populate the arguments if there's no arguments.
                if (args.Length != 0)
                {
                    // populate the arguments
                    for (int i = 0; i < argv.Length; i++)
                    {
                        frame.Add("arg" + i, FetchValue(BadKeyCompiler.ReplaceStrings(argv[i], strings).Trim()));
                    }
                }

                // add a return pointer to the frame
                frame.Add("!return", "" + ip);

                // and push it onto the stack
                stack.Push(frame);

                // trace the stack frame
                string acc = "";
                acc += (Tabbage() + label + " (foreign) called from line " + ip + " with arguments ");
                foreach (KeyValuePair<string, string> pair in frame)
                    acc += (pair.Key + ":\"" + pair.Value + "\" ");
                owner.Debug(acc);

                // for some reason you can't just call owner.Delegates[label](this)
                // it seems like they should be the same thing tho. :/
                ValidCall call = owner.Delegates[label];
                call(this);
            }
            
            else
            {
                owner.Debug("Invalid Call: " + label);
            }
        }

        /// <summary>
        /// Method to signal the thread that it is time to return from the last function called.
        /// </summary>
        /// <remarks>
        /// Can be called by a different method than the external delegate that is likely calling this!
        /// In order to block the pseudo-thread you should wait to call this until AFTER an event has finished.
        /// </remarks>
        /// <param name="rvalue"></param>
        public void Return(string rvalue)
        {
            // grab a list of return values out.
            List<string> returns = new List<string>();

            string[] strings;
            rvalue = BadKeyCompiler.RemoveStrings(rvalue, out strings);

            foreach (string ret in Regex.Split(rvalue, @","))
            {
                returns.Add(FetchValue(BadKeyCompiler.ReplaceStrings(ret.Trim(), strings)));
            }

            // move the ip back to where the last function was called from
            ip = int.Parse(stack.Peek()["!return"]);

            // and pop the top off of the stack
            stack.Pop();
            owner.Debug(Tabbage() + "  Returning to line " + ip +
                " with value !r[" + String.Join(", ", returns.ToArray()) + "]");

            // if there's nowhere to go back to it means this thread has finished executing and can die
            if (stack.Count != 0)
            {
                // if there is somewhere to go back to, add the return values into its stack frame.
                for (int i = 0; i < returns.Count; i++)
                {
                    stack.Peek()["!r" + i] = returns[i];
                }

                EvalNext();
            }
            else
            {
                // let the vmachine know that we can die.
                owner.removeThread(this);
                owner.Debug("Closing thread: " + this.GetHashCode());
            }
        }

        /// <summary>
        /// Return overload for returning null.
        /// </summary>
        public void Return()
        {
            Return("null");
        }

        /// <summary>
        /// Evaluate the next line in the script
        /// </summary>
        /// <remarks>
        /// All keywords and flow control are hardcoded here.
        /// </remarks>
        public void EvalNext()
        {
            ip++;
            string word = owner.Script[ip].Split(' ')[0];

            if (word.Equals("return"))
            {
                // return from a function
                if (owner.Script[ip].Contains(' '))
                    Return(owner.Script[ip].Remove(0, 7));
                else
                    Return();
                return;
            }

            else if (word.Equals("put"))
            {
                // use to place values directly into !r
                string places = owner.Script[ip].Remove(0, 4);

                string[] strings;
                places = BadKeyCompiler.RemoveStrings(places, out strings);

                for (int i = 0; i < Regex.Split(places, @",").Count(); i++)
                {
                    string from = BadKeyCompiler.ReplaceStrings(Regex.Split(places, @",")[i], strings).Trim();
                    stack.Peek()["!r" + i] = FetchValue(from);
                }

                EvalNext();
                return;
            }

            else if (word.Equals("into"))
            {
                // call after a function is called to capture its return value
                string places = owner.Script[ip].Remove(0, 5);

                for (int i = 0; i < Regex.Split(places, @",").Count(); i++)
                {
                    string into = Regex.Split(places, @",")[i].Trim();

                    // figure out where to put it D:
                    if (into.StartsWith("@"))
                    {
                        if (owner.HasPersistent(into))
                            owner.StorePersistent(into, stack.Peek()["!r" + i]);
                        else if (owner.HasGlobal(into))
                            owner.StoreGlobal(into, stack.Peek()["!r" + i]);
                        else
                            throw new Exception("Script Error: Global '" + into + "' hasn't been declared yet.");
                    }
                    else
                    {
                        stack.Peek()[into] = stack.Peek()["!r" + i];
                    }
                }

                EvalNext();
                return;
            }

            else if (word.Equals("jump"))
            {
                // basic goto
                owner.Debug(Tabbage() + "jumping to line "
                    + owner.Labels[owner.Script[ip].Split(' ')[1]]);
                ip = owner.Labels[owner.Script[ip].Split(' ')[1]];
                EvalNext();
                return;
            }

            else if (word.Equals("jumpunless"))
            {
                // conditional goto
                if (FetchValue(owner.Script[ip].Split(' ')[1]).Equals("btrue"))
                {
                    EvalNext();
                    return;
                }
                else
                {
                    owner.Debug(Tabbage() + "jumping to line "
                        + owner.Labels[owner.Script[ip].Split(' ')[2]]);
                    ip = owner.Labels[owner.Script[ip].Split(' ')[2]];
                    EvalNext();
                    return;
                }
            }

            else if (word.Equals("fork"))
            {
                // spawn new thread
                owner.Debug(Tabbage() + "forking to " + owner.Script[ip].Split(' ')[1]);

                string args = "";
                int k = 0;
                for (int j = 0; j < owner.Script[ip].Length; j++)
                {
                    if (k >= 2)
                    {
                        args += owner.Script[ip].ToCharArray()[j];
                    }
                    else if (owner.Script[ip].ToCharArray()[j] == ' ')
                    {
                        k++;
                    }
                }

                string label = owner.Script[ip].Split(' ')[1];
                args = args.Substring(1, args.Length - 2);
                owner.Call(label, args);
                EvalNext();
                return;
            }

            else if (word.Equals("global"))
            {
                // declare global
                string[] args = owner.Script[ip].Split(' ');
                if (args[1].Equals("persistent"))
                {
                    owner.Debug(Tabbage() + "creating new persistent global @" + args[2]);
                    owner.StorePersistent("@" + args[2], "znull");
                }
                else
                {
                    owner.Debug(Tabbage() + "creating new global @" + args[1]);
                    owner.StoreGlobal("@" + args[1], "znull");
                }
                EvalNext();
                return;
            }

            else if (word.Equals(":"))
            {
                // ignore labels
                EvalNext();
                return;
            }

            else if (word.Equals("requiring"))
            {
                string[] args = owner.Script[ip].Split(' ');
                if (!owner.Delegates.ContainsKey(args[1]))
                {
                    throw new Exception("Required hook " + args[1] + " has not been registered.");
                }
                EvalNext();
                return;
            }

            else if (word.Equals("def"))
            {
                throw new Exception("Unexpected end of function");
            }

            else
            {
                // if it's not anything else, attempt to interpret it as a function call
                string args = "";
                int k = 0;
                for (int j = 0; j < owner.Script[ip].Length; j++)
                {
                    if (k >= 1)
                    {
                        args += owner.Script[ip].ToCharArray()[j];
                    }
                    else if (owner.Script[ip].ToCharArray()[j] == ' ')
                    {
                        k++;
                    }
                }

                args = args.Substring(1, args.Length - 2);
                Call(word, args);
                return;
            }
        }


        /// <summary>
        /// Symantically identify a data type and prepend a marker for storage in virtual machine
        /// </summary>
        /// <param name="item">unidentified data type</param>
        /// <returns>data suitable to be handled by virtual machine</returns>
        public string FetchValue(string item)
        {
            // search for unescaped evaluation brackets
            
            bool escaped = false;
            bool brackets = false;

            for (int i = 0; i < item.Length; i++)
            {
                char c = item[i];

                if (c == '\\')
                    escaped = !escaped;

                if (c == '[' && !escaped)
                {
                    brackets = true;
                    break;
                }

                if (c != '\\')
                    escaped = false;
            }

            // square brackets are like 'variable variables' in php and are used to fake arrays.
            // example: x = "y"; y = "3"; [x] => 3
            // other example: arg0 = "hello"; arg1 = "world"; x = 0; arg[x] => "hello"
            if (brackets)
            {
                // track nesting of brackets. really we only care about the root and the first level
                int nested = 0;

                // separate the inline evaluated bits from the not-so-evaluated.
                List<string> unevaluated = new List<string>();
                List<string> evaluated = new List<string>();

                string section = "";

                escaped = false;

                for (int i = 0; i < item.Length; i++)
                {
                    char c = item[i];

                    if (c == '\\')
                        escaped = !escaped;

                    if (c == '[' && !escaped)
                    {
                        if (nested == 0)
                        {
                            unevaluated.Add(section);
                            section = "";
                        }
                        nested++;
                    }

                    section += c;

                    if (c == ']' && !escaped)
                    {
                        if (nested == 1)
                        {
                            evaluated.Add(section);
                            section = "";
                        }
                        nested--;
                    }

                    if (c != '\\')
                        escaped = false;
                }

                unevaluated.Add(section);

                owner.Debug("<" + string.Join(":", evaluated.ToArray()) + ">");

                string final = "";

                // recombine evaluated with unevaluated, fetching values for evaluated as we go.
                for (int i = 0; i < evaluated.Count; i++)
                {
                    final += unevaluated[i];
                    final += ToS(FetchValue(evaluated[i].Substring(1, evaluated[i].Length - 2)));
                }
                final += unevaluated.Last();

                // finally fetch the value of the resulting variable name.
                return FetchValue(final);
            }

            double value;

            // number
            if (Regex.IsMatch(item, @"^-?\d+(\.\d+)?$"))
            {
                try
                {
                    value = double.Parse(item);
                    return "n" + value;
                }
                catch { }
            }

            // string
            if (item.StartsWith("\""))
            {
                item = item.Substring(1, item.Length - 2);

                // match a \ followed by either a non-\ character or a newline
                // and remove it.
                item = Regex.Replace(item, @"\\(.)", "$1");
                return "s" + item;
            }

            // boolean true
            else if (item.ToLower().Equals("true"))
                return "btrue";

            // bolean false
            else if (item.ToLower().Equals("false"))
                return "bfalse";

            // null
            else if (item.ToLower().Equals("null"))
                return "znull";

            // global
            else if (item.StartsWith("@"))
            {
                if (owner.HasGlobal(item))
                    return owner.LoadGlobal(item);
                else if (owner.HasPersistent(item))
                    return owner.LoadPersistent(item);
                else
                    throw new Exception("Script Error: Global '" + item + "' hasn't been declared yet.");
            }

            // local
            else if (stack.Peek().ContainsKey(item))
                return stack.Peek()[item];

            // undefined
            else
                return "zundefined";
        }

        /// <summary>
        /// Convert an encoded piece of virtual machine data to a c# boolean
        /// </summary>
        /// <param name="item">virtual machine data</param>
        /// <returns>boolean</returns>
        public Boolean ToB(string item)
        {
            if (item.Equals("bfalse"))
                return false;
            else
                return true;
        }

        /// <summary>
        /// Convert an encoded piece of virtual machine data to a c# double
        /// </summary>
        /// <param name="item">virtual machine data</param>
        /// <returns>number</returns>
        public double ToN(string item)
        {
            return double.Parse(item.TrimStart('n'));
        }

        /// <summary>
        /// Convert an encoded piece of virtual machine data to a c# string
        /// </summary>
        /// <param name="item">virtual machine data</param>
        /// <returns>string</returns>
        public string ToS(string item)
        {
            return item.Substring(1);
        }

        /// <summary>
        /// Use stack size to determine indentation on messages
        /// </summary>
        /// <returns>tabs</returns>
        private string Tabbage()
        {
            string acc = "";
            for (int i = 0; i < stack.Count; i++)
            {
                acc += "  ";
            }
            return acc;
        }
    }
}
